/*
 * The server-side thread for a client. Client threads must 
 * be careful when updating the state of an opponent.
 */

package egs.server;

import java.io.IOException;
import java.util.LinkedList;

import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;

import egs.comms.Connection;
import egs.comms.EGSCommunicationException;
import egs.comms.ServerConnection;
import egs.games.Game;
import egs.packets.AuthLoginErrPacket;
import egs.packets.AuthLoginPacket;
import egs.packets.BoardSendPacket;
import egs.packets.EndGamePacket;
import egs.packets.GameListPacket;
import egs.packets.InvalidGamePacket;
import egs.packets.InvalidPacketException;
import egs.packets.MoveResponsePacket;
import egs.packets.MoveSendPacket;
import egs.packets.OppAckPacket;
import egs.packets.OppChoicePacket;
import egs.packets.OppListPacket;
import egs.packets.OppNackPacket;
import egs.packets.OppOfferPacket;
import egs.packets.Packet;
import egs.packets.PacketException;
import egs.packets.SelectGamePacket;
import egs.packets.UseSecurePacket;
import egs.packets.UseSecureResponsePacket;
import egs.packets.EndGamePacket.ReasonCode;

public class ClientThread implements Runnable {
	private Thread clientThread;
	private GameManager gameManager;
	private AuthenticationManager authManager;
	private ClientManager clientManager;
	private ClientState clientState;
	
	public ClientThread(AuthenticationManager authManager, GameManager gameManager, ClientState clientState) {
		this.clientThread = new Thread(this);
		this.gameManager = gameManager;
		this.authManager = authManager;
		this.clientManager = null;
		this.clientState = clientState;
		this.clientThread.start();
	}

	public void run() {
		while (true) {
			try{
				Packet pkt = clientState.getPlayerConnection().getPacket();
				handlePacket(pkt);
			} catch (InvalidPacketException pe) {
				System.out.println("Connection closed prematurely.");
				//System.err.println("Invalid message: " + pe );
				closeConnection();
				return;
			} catch (EGSCommunicationException e) {
				System.err.println("Error sending data: " + e );
				closeConnection();
                return;
			}
		}
	}

	private void handlePacket(Packet pkt) throws EGSCommunicationException {
		switch (clientState.getState()) {
		case ClientState.CONTROL:
			handlePacketForControl(pkt);
			break;
		case ClientState.UNAUTHENTICATED:
			handlePacketForUnauthenticated(pkt);
			break;
		case ClientState.AUTHENTICATED:
			handlePacketForAuthenticated(pkt);
			break;
		case ClientState.GAME_SELECTED:
			handlePacketForGameSelected(pkt);
			break;
		case ClientState.OPPONENT_WAIT:
			handlePacketForOpponentWait(pkt);
			break;
		case ClientState.IN_GAME:
			handlePacketForInGame(pkt);
			break;
		case ClientState.MAKE_MOVE:
			handlePacketForMakeMove(pkt);
			break;
		}
	}

	private void handlePacketForMakeMove(Packet pkt) throws EGSCommunicationException {
		if (pkt.get_message_type() == Packet.MSG_MOVE_SEND) {
			MoveSendPacket moveSendPacket = (MoveSendPacket) pkt;
			int[][] board = moveSendPacket.get_board();
			int playerNumber = clientState.getPlayerNumber();
			Game game = clientState.getSelectedGame();
			short gameName = clientState.getSelectedGameKey().getGameName();
			short gameVersion = clientState.getSelectedGameKey().getGameVersion();
			int result = game.verifyMove(board, playerNumber);
			if (result == Game.VALID) {
				game.makeMove(board, playerNumber);
				short responseCode = MoveResponsePacket.MOVE_RESPONSE_VALID_MOVE;
				Packet moveResponsePacket = null;
				try {
					moveResponsePacket = new MoveResponsePacket(responseCode);
				} catch (PacketException e) {
					throw new EGSCommunicationException("Could not create move_response packet: " + e.toString());
				}
				clientState.lock();
				clientState.setState(ClientState.IN_GAME);
				clientState.getPlayerConnection().sendPacket(moveResponsePacket);
				clientState.unlock();
				
				Packet endGamePacket;
				EndGamePacket.ReasonCode clientReasonCode;
				EndGamePacket.ReasonCode oppReasonCode;
				int gameOver = game.getGameOver();
				ClientState oppState = clientState.getOppState();
				switch(gameOver) {
				case 0:
					// continue game
					int oppPlayerNumber = oppState.getPlayerNumber();
					int oppBoard[][] = game.getRawBoard();
					Packet boardSendPacket = new BoardSendPacket(gameName, gameVersion, oppPlayerNumber, oppBoard);
					oppState.lock();
					oppState.setState(ClientState.MAKE_MOVE);
					oppState.getPlayerConnection().sendPacket(boardSendPacket);
					oppState.unlock();
					break;
				case 1:
					// player 1 won
					if (playerNumber == 1)
					{
						clientReasonCode = EndGamePacket.ReasonCode.win;
						oppReasonCode = EndGamePacket.ReasonCode.loss;
					}
					else
					{
						clientReasonCode = EndGamePacket.ReasonCode.loss;
						oppReasonCode = EndGamePacket.ReasonCode.win;
					}
					
					endGamePacket = new EndGamePacket(gameName, gameVersion, clientReasonCode);
					clientState.lock();
					clientState.setState(ClientState.AUTHENTICATED);
					clientState.getPlayerConnection().sendPacket(endGamePacket);
					clientState.unlock();
					
					endGamePacket = new EndGamePacket(gameName, gameVersion, oppReasonCode);
					oppState.lock();
					oppState.setState(ClientState.AUTHENTICATED);
					oppState.getPlayerConnection().sendPacket(endGamePacket);
					oppState.unlock();
					break;
				case 2:
					// player 1 won
					if (playerNumber == 1)
					{
						clientReasonCode = EndGamePacket.ReasonCode.loss;
						oppReasonCode = EndGamePacket.ReasonCode.win;
					}
					else
					{
						clientReasonCode = EndGamePacket.ReasonCode.win;
						oppReasonCode = EndGamePacket.ReasonCode.loss;
					}
					
					endGamePacket = new EndGamePacket(gameName, gameVersion, clientReasonCode);
					clientState.lock();
					clientState.setState(ClientState.AUTHENTICATED);
					clientState.getPlayerConnection().sendPacket(endGamePacket);
					clientState.unlock();
					
					endGamePacket = new EndGamePacket(gameName, gameVersion, oppReasonCode);
					oppState.lock();
					oppState.setState(ClientState.AUTHENTICATED);
					oppState.getPlayerConnection().sendPacket(endGamePacket);
					oppState.unlock();
					break;
				case 3:
					// tie
					
					endGamePacket = new EndGamePacket(gameName, gameVersion, EndGamePacket.ReasonCode.tie);
					clientState.lock();
					clientState.setState(ClientState.AUTHENTICATED);
					clientState.getPlayerConnection().sendPacket(endGamePacket);
					clientState.unlock();
					
					endGamePacket = new EndGamePacket(gameName, gameVersion, EndGamePacket.ReasonCode.tie);
					oppState.lock();
					oppState.setState(ClientState.AUTHENTICATED);
					oppState.getPlayerConnection().sendPacket(endGamePacket);
					oppState.unlock();
					break;
				default:
					throw new EGSCommunicationException("Game ended in unexpected way: " + gameOver);
				}

			} else {
				Packet moveResponsePacket = null;
				short responseCode;
				if (result == Game.INVALID_BOARD) {
					responseCode = MoveResponsePacket.MOVE_RESPONSE_INVALID_BOARD;
				} else if (result == Game.INVALID_MOVE) {
					responseCode = MoveResponsePacket.MOVE_RESPONSE_ILLEGAL_MOVE;
				} else {
					responseCode = MoveResponsePacket.MOVE_RESPONSE_ILLEGAL_MOVE;
				}
				try {
					moveResponsePacket = new MoveResponsePacket(responseCode);
				} catch (PacketException e) {
					throw new EGSCommunicationException("Could not create move_response packet: " + e.toString());
				}
				clientState.lock();
				clientState.getPlayerConnection().sendPacket(moveResponsePacket);
				clientState.unlock();
			}
			
		} else if (pkt.get_message_type() == Packet.MSG_DISCONNECT) {
			//DisconnectPacket disconnectPacket = (DisconnectPacket) pkt;
			disconnect();
			
		} else {
			System.err.println(
					"Unexpected packet message type " + 
					pkt.get_message_type() + 
					" received in MAKE_MOVE state.");
		}
	}

	private void handlePacketForInGame(Packet pkt) {
		if (pkt.get_message_type() == Packet.MSG_DISCONNECT) {
			//DisconnectPacket disconnectPacket = (DisconnectPacket) pkt;
			disconnect();
		
		} else {
			System.err.println(
					"Unexpected packet message type " + 
					pkt.get_message_type() + 
					" received in IN_GAME state.");
		}
	}

	private void handlePacketForOpponentWait(Packet pkt) throws EGSCommunicationException {
		String username = clientState.getUsername();
		if (!clientManager.isOppWaiting(username)) {
			System.err.println(
					"Unexpected packet message type " + 
					pkt.get_message_type() + 
					" received from client awaiting a response to its offer");
			return;
		}
		
		String offeringOpp = clientManager.getOppOffer(username);
		ClientState oppState = clientManager.getClientState(offeringOpp);
		short gameName = pkt.get_game_name();
		short gameVersion = pkt.get_game_version();
		if (pkt.get_message_type() == Packet.MSG_OPP_ACK) {
			// client was given an offer which it accepted
			clientManager.removeWaitingOpp(username);
			OppAckPacket oppAckPacket = null;
			try {
				oppAckPacket = new OppAckPacket(gameName, gameVersion, username);
			} catch (PacketException e) {
				// send opp_timeout since there is no server_error reason code for this packet
				e.printStackTrace();
				oppAckPacket = null;
				clientManager.unsetOppOffer(username);
				OppNackPacket.ReasonCode reasonCode = OppNackPacket.ReasonCode.opp_timeout;
				OppNackPacket oppNackPacket = new OppNackPacket(reasonCode);
				oppState.lock();
				oppState.setState(ClientState.GAME_SELECTED);
				oppState.getPlayerConnection().sendPacket(oppNackPacket);
				oppState.unlock();
			}
			if (oppAckPacket != null) {
				GameManager.GameKey key = clientState.getSelectedGameKey();
				Game game = gameManager.buildGame(key);
				
				oppState.lock();
				oppState.setOppState(clientState);
				oppState.setState(ClientState.IN_GAME);
				oppState.setSelectedGame(game);
				oppState.setPlayerNumber(2);
				oppState.getPlayerConnection().sendPacket(oppAckPacket);
				oppState.unlock();
				
				clientState.lock();
				clientState.setOppState(oppState);
				clientState.setState(ClientState.IN_GAME);
				clientState.setSelectedGame(game);
				clientState.setPlayerNumber(1);
				clientState.unlock();
				
				// client which was waiting and accepted the offer moves first
				int playerNumber = clientState.getPlayerNumber();
				int board[][] = clientState.getSelectedGame().getRawBoard();
				BoardSendPacket boardSendPacket = new BoardSendPacket(gameName, gameVersion, playerNumber, board);
				clientState.lock();
				clientState.setState(ClientState.MAKE_MOVE);
				clientState.getPlayerConnection().sendPacket(boardSendPacket);
				clientState.unlock();
			}

		} else if (pkt.get_message_type() == Packet.MSG_OPP_NACK) {
			// client was given an offer which it rejected
			clientManager.unsetOppOffer(username);
			OppNackPacket.ReasonCode reasonCode = OppNackPacket.ReasonCode.opp_reject;
			OppNackPacket oppNackPacket = new OppNackPacket(reasonCode);
			oppState.lock();
			oppState.setState(ClientState.GAME_SELECTED);
			oppState.getPlayerConnection().sendPacket(oppNackPacket);
			oppState.unlock();
			
		} else if (pkt.get_message_type() == Packet.MSG_DISCONNECT) {
			//DisconnectPacket disconnectPacket = (DisconnectPacket) pkt;
			disconnect();
			
		} else {
			System.err.println(
					"Unexpected packet message type " + 
					pkt.get_message_type() + 
					" received in OPPONENT_WAIT state.");
		}
	}

	private void handlePacketForGameSelected(Packet pkt) throws EGSCommunicationException {
		if (pkt.get_message_type() == Packet.MSG_OPP_CHOICE) {
			OppChoicePacket oppChoicePacket = (OppChoicePacket) pkt;
			boolean wait = oppChoicePacket.get_wait();
			short gameName = oppChoicePacket.get_game_name();
			short gameVersion = oppChoicePacket.get_game_version();
			String oppChoice = oppChoicePacket.get_user_name().trim();
			String username = clientState.getUsername();
			ClientState oppState = null;
			Packet response = null;
			Packet offer = null;
			
			if (wait) {
				clientManager.addWaitingOpp(username);
			} else {
				if (clientManager.getClientState(oppChoice) == null) {
					// invalid opponent choice
					OppNackPacket.ReasonCode reasonCode = OppNackPacket.ReasonCode.invalid_opponent;
					response = new OppNackPacket(reasonCode);
				} else if (clientManager.setOppOffer(oppChoice, username)) {
					// opponent is valid and is awaiting an offer
					oppState = clientManager.getClientState(oppChoice);
					try {
						offer = new OppOfferPacket(gameName, gameVersion, username);
					} catch (PacketException e) {
						throw new EGSCommunicationException("Could not create opp_offer packet: " + e.toString());
					}
				} else {
					// opponent left or already has an outstanding offer
					OppNackPacket.ReasonCode reasonCode = OppNackPacket.ReasonCode.opp_left;
					response = new OppNackPacket(reasonCode);
				}
			}
			
			if (response != null) {
				clientState.lock();
				clientState.getPlayerConnection().sendPacket(response);
				clientState.unlock();
			} else {
				clientState.lock();
				clientState.setState(ClientState.OPPONENT_WAIT);
				clientState.unlock();
				if (offer != null && oppState != null) {
					oppState.lock();
					oppState.getPlayerConnection().sendPacket(offer);
					oppState.unlock();
				}
			}
			
		} else if (pkt.get_message_type() == Packet.MSG_DISCONNECT) {
			//DisconnectPacket disconnectPacket = (DisconnectPacket) pkt;
			disconnect();
			
		} else {
			System.err.println(
					"Unexpected packet message type " + 
					pkt.get_message_type() + 
					" received in GAME_SELECTED state.");
		}
	}

	private void handlePacketForAuthenticated(Packet pkt) throws EGSCommunicationException {
		if (pkt.get_message_type() == Packet.MSG_SELECT_GAME) {
			SelectGamePacket selectGamePacket = (SelectGamePacket) pkt;
			//short gameName = selectGamePacket.get_game_name();
			//short gameVersion = selectGamePacket.get_game_version();
			short gameName = selectGamePacket.get_packet_game_name();
			short gameVersion = selectGamePacket.get_packet_game_version();
			String username = clientState.getUsername();
			Packet response = null;
			if (gameManager.isSupportedGame(gameName, gameVersion)) {
				clientManager = gameManager.getClientManager(gameName, gameVersion);
				LinkedList<String> oppList = clientManager.getOppList();
				try {
					response = new OppListPacket(gameName, gameVersion, oppList);
				} catch (PacketException e) {
					response = new InvalidGamePacket(InvalidGamePacket.ReasonCode.server_error, gameName, gameVersion);
				}
			} else if (gameManager.isKnownGame(gameName, gameVersion)) {
				response = new InvalidGamePacket(InvalidGamePacket.ReasonCode.unsupported_game, gameName, gameVersion);	
			} else {
				response = new InvalidGamePacket(InvalidGamePacket.ReasonCode.unknown_game, gameName, gameVersion);
			}
			
			clientState.lock();
			if (response.get_message_type() == Packet.MSG_OPP_LIST) {
				clientManager.setClientState(username, clientState);
				clientState.setState(ClientState.GAME_SELECTED);
				GameManager.GameKey selectedGameKey = gameManager.getGameKey(gameName, gameVersion);
				clientState.setSelectedGameKey(selectedGameKey);
			}
			clientState.getPlayerConnection().sendPacket(response);
			clientState.unlock();
			
		} else if (pkt.get_message_type() == Packet.MSG_DISCONNECT) {
			//DisconnectPacket disconnectPacket = (DisconnectPacket) pkt;
			disconnect();
		} else {
			System.err.println(
					"Unexpected packet message type " + 
					pkt.get_message_type() + 
					" received in AUTHENTICATED state.");
		}
	}

    private void handlePacketForUnauthenticated(Packet pkt) throws EGSCommunicationException {
    	if (pkt.get_message_type() == Packet.MSG_AUTH_LOGIN) {
    		AuthLoginPacket authLoginPacket = (AuthLoginPacket) pkt;
    		String user_name = authLoginPacket.get_user_name();
    		String password = authLoginPacket.get_password();
    		Packet response = null;
    		if (isAuthenticated(user_name, password)) {
    			clientState.setUsername(user_name);
    			short[][] game_list = new short[3][2];
    			game_list[0][0] = Packet.GAME_NAME_CHECKERS;
    			game_list[0][1] = 1;
    			game_list[1][0] = Packet.GAME_NAME_BLACKJACK;
    			game_list[1][1] = 1;
    			game_list[2][0] = Packet.GAME_NAME_TICTACTOE;
    			game_list[2][1] = 1;
    			try {
    				response = new GameListPacket( game_list );
    			} catch ( PacketException pe ) {
    				response = new AuthLoginErrPacket(AuthLoginErrPacket.ReasonCode.server_error);
    			}
    		} else {
    			response = new AuthLoginErrPacket(AuthLoginErrPacket.ReasonCode.invalid_login);
    		}
    		
    		clientState.lock();
			if (response.get_message_type() == Packet.MSG_GAME_LIST)
				clientState.setState(ClientState.AUTHENTICATED);
			clientState.getPlayerConnection().sendPacket(response);
			clientState.unlock();
			
    	} else {
    		System.err.println(
    				"Unexpected packet message type " + 
    				pkt.get_message_type() + 
    		" received in UNAUTHENTICATED state.");
    	}
    }

	private boolean isAuthenticated(String user_name, String password) {
		return authManager.isAuthenticated(user_name, password);
	}

	private void handlePacketForControl(Packet pkt) throws EGSCommunicationException {
		boolean securityRequested = false;
		short securePort = -1;
		
		if (pkt.get_message_type() == Packet.MSG_USE_SECURE) {
			UseSecurePacket useSecurePacket = (UseSecurePacket) pkt;
			UseSecureResponsePacket.ResponseCode responseCode;
			if (useSecurePacket.get_security_code() == UseSecurePacket.SecurityCode.none) {
				responseCode = UseSecureResponsePacket.ResponseCode.accepted;
			} else if(useSecurePacket.get_security_code() == UseSecurePacket.SecurityCode.tls){
				responseCode = UseSecureResponsePacket.ResponseCode.accepted;
				securityRequested = true; 
				
			}
				else {
			    responseCode = UseSecureResponsePacket.ResponseCode.unsupported_protocol;
			}
			
			Packet response = new UseSecureResponsePacket(responseCode);
			
			clientState.lock();
			if (responseCode == UseSecureResponsePacket.ResponseCode.accepted){
				clientState.setState(ClientState.UNAUTHENTICATED);
				if(securityRequested){
					securePort = EGSServer.getSecurePort();
					((UseSecureResponsePacket)response).setPort(securePort);
				}
			}
			clientState.getPlayerConnection().sendPacket(response);
			clientState.unlock();
			
			if (securityRequested){
				try{
					SSLServerSocketFactory socketFactory = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
					SSLServerSocket secureSocket = (SSLServerSocket) socketFactory.createServerSocket(securePort);
					SSLSocket new_socket = (SSLSocket) secureSocket.accept();
					secureSocket.close();
					EGSServer.returnPort(securePort);
					Connection secureConnection = new ServerConnection(new_socket); 
					this.clientState.getPlayerConnection().closeConnection();
					this.clientState.setPlayerConnection(secureConnection);
				}catch(IOException e){
					throw new EGSCommunicationException("Could not open secure port " + e.toString());
				}
			}
			
			
		} else {
			System.err.println(
					"Unexpected packet message type " + 
					pkt.get_message_type() + 
					" received in CONTROL state.");
		}
	}
	
	private void closeConnection() {
		disconnect();
		clientState.getPlayerConnection().closeConnection();
		return;
	}
	
	private void disconnect() {
		notifyOpponent();
		clearState(clientState);
		clientState.setUsername(null);
		clientState.setState(ClientState.CONTROL);
	}
	
	private void notifyOpponent() {
		ClientState oppState = clientState.getOppState();
		GameManager.GameKey key = clientState.getSelectedGameKey();
		if (oppState != null && key != null) {
			short gameName = key.getGameName();
			short gameVersion = key.getGameVersion();
			EndGamePacket.ReasonCode reasonCode = EndGamePacket.ReasonCode.opp_timeout;
			Packet endGamePacket = new EndGamePacket(gameName, gameVersion, reasonCode);
			oppState.lock();
			try {
				oppState.getPlayerConnection().sendPacket(endGamePacket);
			} catch (EGSCommunicationException e) {
				System.err.println("Error notifying opponent of client closed.");
				e.printStackTrace();
				oppState.getPlayerConnection().closeConnection();
			}
			clearState(oppState);
			oppState.unlock();
		}
	}
	
	private void clearState(ClientState clientState) {
		String username = clientState.getUsername();
		clientState.setState(ClientState.AUTHENTICATED);
		clientState.setSelectedGameKey(null);
		clientState.setOppState(null);
		clientState.setSelectedGame(null);
		clientState.setPlayerNumber(0);
		if (clientManager != null) {
			clientManager.removeClientState(username);
			clientManager.removeWaitingOpp(username);
			clientManager.unsetOppOffer(username);
		}
	}
}
